﻿//   Scanner.cs
//
//   Copyright 2009 Eric Llewellyn
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LibEELbeta
{
    public class Scanner
    {
        private string buffer;
        private int position;
        private List<int> lineOffsets = new List<int>();

        public Scanner()
        {
            this.buffer = "";
            this.position = 0;
            this.lineOffsets.Add(0);
        }

        //Add every line of "input" to the buffer
        //Get tokens untill NULL is reached
        //Return all the tokens read
        public Token[] ScanLine(string input)
        {
            List<Token> tokens = new List<Token>();
            Token t;

            if (input != null)
            {
                string[] lines = input.Replace("\r", "").Split(new char[] { '\n' });
                foreach (string line in lines)
                {
                    buffer += line + "\r\n";
                    lineOffsets.Add(position + buffer.Length);
                }
            }

            do
            {
                t = GetToken();
                if (t != null)
                    tokens.Add(t);
            } while (t != null);

            return tokens.ToArray();
        }

        //------------

        //Calculate the current line and column numbers
        //Start largest match at a single character identified as an "Unknown" token
        //Return null if the first character is < 0
        //Find all the matching tokens
        //Select the largest if any matches are found
        //Return null if matches == null that means we're waiting for more characters
        //Advance the buffer and return the Token
        private Token GetToken()
        {
            List<Token> matches;
            Token largest;

            int line = 0;
            for (line = 0; line < lineOffsets.Count; line++)
                if (lineOffsets[line] > position)
                    break;
            int column = position - lineOffsets[line - 1] + 1;

            largest = new Token(TokenTypes.Unknown, Read(1), line, column);

            if ((short)CharAt(0) < 0)
                return null;

            matches = GetMatches(line, column);
            if (matches == null)
                return null;

            foreach (Token t in matches)
            {
                if (t.value.Length >= largest.value.Length)
                    largest = t;
            }
            Advance(largest.value.Length);

            return largest;
        }

        //Returns the character at "index" in the buffer
        //Returns -1 if not enough characters are available
        private char CharAt(int index)
        {
            short eof = -1;

            if (index < buffer.Length)
                return buffer[index];
            else
                return (char)eof;
        }

        //Returns the first "numChars" characters from the buffer
        //Returns "" if not enough characters are available
        private string Read(int numChars)
        {
            if (numChars > buffer.Length)
                return "";

            return buffer.Substring(0, numChars);
        }

        //Remove the first "numChars" from the buffer
        //and increase the position counter by that amount
        //Do nothing if not enough characters are available
        private void Advance(int numChars)
        {
            if (numChars > buffer.Length)
                return;

            position += numChars;
            buffer = buffer.Substring(numChars);
        }

        //------------

        //Comment:  //[^\r]*
        //-1 fail
        int MatchComment()
        {
            int i = 0;

            if (CharAt(0) == '/' && CharAt(1) == '/')
            {
                i += 2;
                while (CharAt(i) != '\r')
                    i++;
                return i;
            }

            return -1;
        }

        //Number:   [0-9]+(\\.[0-9]+)?
        //-1 fail
        int MatchNumber()
        {
            int i = 0;

            if (!char.IsNumber((char)CharAt(i)))
                return -1;
            while (char.IsNumber((char)CharAt(i)))
                i++;

            if (CharAt(i) == '.')
            {
                i++;
                if (!char.IsNumber((char)CharAt(i)))
                    return -1;
                while (char.IsNumber((char)CharAt(i)))
                    i++;
            }

            return i;
        }

        //String:   \"([^\"])*\"
        //-1 fail
        //-2 out of chars
        int MatchString()
        {
            int i = 0;

            if (CharAt(0) == '\"')
            {
                i++;
                while ((short)CharAt(i) >= 0 && CharAt(i) != '\"')
                    i++;
                if ((short)CharAt(i) < 0)
                    return -2;
                else
                    return i + 1;
            }

            return -1;
        }

        //ID:       [a-zA-Z_$][0-9a-zA-Z_]*
        //-1 fail
        int MatchId()
        {
            int i = 0;

            if (char.IsLetter((char)CharAt(0)) || CharAt(0) == '_' || CharAt(0) == '$')
            {
                i++;
                while (char.IsLetterOrDigit((char)CharAt(i)) || CharAt(i) == '_')
                    i++;
                return i;
            }

            return -1;
        }

        //Text:     Must match the exact text specified
        //-1 fail
        int MatchText(string text)
        {
            int j = 0;

            while (j < text.Length && CharAt(j) == text[j])
                j++;

            if (j == text.Length)
                return j;
            else
                return -1;
        }

        //ID
        //"read"        "write"     "if"        "else"      "end"
        //"loop"        "while"     "function"  "return"    "arg"
        //"="           "["         "]"         "("         ")"
        //","           "+"         "-"         "*"         "/"
        //"<"           ">"         "!"         ";"         "."
        //"=="          "!="        "<="        ">="
        //NUMBER        STRING      COMMENT     "true"      "false"
        //"null"        "[]"
        //
        //Return empty list if no matches
        //Return null if any match reports out of chars
        private List<Token> GetMatches(int line, int col)
        {
            List<Token> matches = new List<Token>();
            int len;

            len = MatchId();
            if (len > 0)
                matches.Add(new Token(TokenTypes.Id, Read(len), line, col));
            else if (len == -2)
                return null;

            //----

            len = MatchText("read");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Read, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("write");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Write, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("if");
            if (len > 0)
                matches.Add(new Token(TokenTypes.If, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("else");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Else, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("end");
            if (len > 0)
                matches.Add(new Token(TokenTypes.End, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("loop");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Loop, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("while");
            if (len > 0)
                matches.Add(new Token(TokenTypes.While, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("function");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Function, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("return");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Return, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("arg");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Arg, Read(len), line, col));
            else if (len == -2)
                return null;

            //----

            len = MatchText("=");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Assign, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("[");
            if (len > 0)
                matches.Add(new Token(TokenTypes.LIndex, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("]");
            if (len > 0)
                matches.Add(new Token(TokenTypes.RIndex, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("(");
            if (len > 0)
                matches.Add(new Token(TokenTypes.LBracket, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText(")");
            if (len > 0)
                matches.Add(new Token(TokenTypes.RBracket, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText(",");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Comma, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("+");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Plus, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("-");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Minus, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("*");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Times, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("/");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Div, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("<");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Lt, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText(">");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Gt, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("!");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Not, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText(";");
            if (len > 0)
                matches.Add(new Token(TokenTypes.SemiColon, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText(".");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Dot, Read(len), line, col));
            else if (len == -2)
                return null;

            //----

            len = MatchText("==");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Eq, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("!=");
            if (len > 0)
                matches.Add(new Token(TokenTypes.NEq, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("<=");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Le, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText(">=");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Ge, Read(len), line, col));
            else if (len == -2)
                return null;

            //----

            len = MatchNumber();
            if (len > 0)
                matches.Add(new Token(TokenTypes.Number, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchString();
            if (len > 0)
                matches.Add(new Token(TokenTypes.String, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchComment();
            if (len > 0)
                matches.Add(new Token(TokenTypes.Comment, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("true");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Bool, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("false");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Bool, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("null");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Null, Read(len), line, col));
            else if (len == -2)
                return null;

            len = MatchText("[]");
            if (len > 0)
                matches.Add(new Token(TokenTypes.Table, Read(len), line, col));
            else if (len == -2)
                return null;

            return matches;
        }
    }
}
